﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Timers;
using System.Windows.Threading;

namespace ActionScheduler.Lib {
    /// <summary>
    /// Use the class to Run a Action with a delay in the Background or on the main GUI Thread
    /// </summary>
    public class ActionScheduler {

        public const int DEFAULT_DELAY = 200;
        private const double TIMER_FREQUENCY = 40;

        private static ActionScheduler _instance;
        private static readonly object _lockObj = new object();

        private readonly object _lockStartAction = new object();
        private readonly object _lockActions = new object();
        private readonly List<ActionSchedulerAction> _actions;
        private readonly Timer _actionStartTimer;


        private ActionScheduler() {
            _actions = new List<ActionSchedulerAction>();
            _actionStartTimer = new Timer(TIMER_FREQUENCY);
            _actionStartTimer.Enabled = false;
            _actionStartTimer.Elapsed += StartActions;
        }

        public static ActionScheduler Instance {
            get {
                if (_instance == null) {
                    lock (_lockObj) {
                        if (_instance == null) {
                            _instance = new ActionScheduler();
                        }
                    }
                }
                return _instance;
            }
        }

        /// <summary>
        /// This the the Main Function to Schedule an Action
        /// </summary>
        /// <param name="action">Well, the Action to Schedule - Use new Action() to create a variable or Just Use a function name or a Lamda () => ... here</param>
        /// <param name="runOnGuiThread">Wether to run the action in the Background (default) or on the main GUI Thread</param>
        /// <param name="scopeKey">A user defined Scope to be able to cancel all scheduled actions with a certain scope</param>
        /// <param name="actionKey">if not given in the full Type Name, Method Name and HasCode of the instance is used. Needed if the Same Method needs to be called with different parameters</param>
        /// <param name="onCompleted">Action to run after the action given in param action is completed</param>
        /// <param name="runOnCompletedOnGuiThread">Wether to run the second action on the main GUI Thread or not - Run Actions that need access to GUI elements like controls on the GUI Thread!</param>
        /// <param name="delayInMilliseconds">The delay to wait until the action is started. Default is defined in const DEFAULT_DELAY</param>
        public static void ScheduleAction(Action action, bool runOnGuiThread = false, string scopeKey = null, string actionKey = null, Action onCompleted = null, bool runOnCompletedOnGuiThread = true, int delayInMilliseconds = DEFAULT_DELAY) {
            Instance.AddAction(action, runOnGuiThread, scopeKey, actionKey, delayInMilliseconds, onCompleted, runOnCompletedOnGuiThread);
        }

        /// <summary>
        /// Cancel the coresponding actions if they are not started yet!
        /// </summary>
        /// <param name="scopeKey">2nd priority. Cancel all actions that were scheduled with the same scope key and not started. If action Key is given in thes parameter is ignored</param>
        /// <param name="actionKey">1st priority. Cancel the action with this action Key</param>
        /// <param name="onlyGuiActions">If true cancels only actions scheduled to run on the main GUI Thread</param>
        public static void CancelActions(string scopeKey = null, string actionKey = null, bool onlyGuiActions = false) {
            Instance.CancelActionsInternal(scopeKey, actionKey, onlyGuiActions);
        }

        private void CancelActionsInternal(string scopeKey, string actionKey, bool onlyGuiActions) {
            var actionsToCancel = new List<ActionSchedulerAction>();
            lock (_lockActions) {
                if (!string.IsNullOrWhiteSpace(actionKey)) {
                    actionsToCancel.AddRange(_actions.Where(a => a.ActionKey == actionKey));
                } else if (!string.IsNullOrWhiteSpace(scopeKey)) {
                    actionsToCancel.AddRange(_actions.Where(a => a.ScopeKey == scopeKey));
                } else {
                    actionsToCancel = _actions;
                }

                if (onlyGuiActions) {
                    actionsToCancel = actionsToCancel.Where(a => a.RunOnGuiThread).ToList();
                }
            }

            foreach (var action in actionsToCancel) {
                CancelAction(action);
            }
        }

        private void CancelAction(ActionSchedulerAction action) {
            if (!action.IsRunning) RemoveAction(action);
        }

        private static string GetActionKey(Action action) {
            var method = action.Method;
            if (method.DeclaringType != null) {
                //Build key of TypeName, MethodName, Instance
                var ret = method.DeclaringType.FullName + "." + method.Name;
                if (action.Target != null) {
                    ret += "_#_" + action.Target.GetHashCode();
                }
                return ret;
            }
            return method.Name;
        }

        private void AddAction(Action action, bool runOnGuiThread, string scopeKey, string actionKey, int delayInMilliseconds, Action onCompleted, bool runOnCompletedOnGuiThread) {
            if (string.IsNullOrWhiteSpace(actionKey)) actionKey = GetActionKey(action);
            RemoveActionsWithSameActionKey(actionKey);

            var newAction = new ActionSchedulerAction {
                Action = action,
                RunOnGuiThread = runOnGuiThread,
                ScopeKey = scopeKey,
                ActionKey = actionKey,
                StartAction = DateTime.Now.AddMilliseconds(delayInMilliseconds),
                OnCompleted = onCompleted,
                RunOnCompletedOnGuiThread = runOnCompletedOnGuiThread,
            };

            lock (_lockActions) {
                _actions.Add(newAction);
            }
            if (delayInMilliseconds <= 0) {
                lock (_lockStartAction) {
                    StartAction(newAction);
                }
            }

            if (!_actionStartTimer.Enabled) _actionStartTimer.Enabled = true;
        }

        private void RemoveActionsWithSameActionKey(string actionKey) {
            lock (_lockActions) {
                foreach (var action in _actions.Where(a => a.ActionKey == actionKey).ToArray()) {
                    _actions.Remove(action);
                }
            }
        }

        private void StartActions(object state, ElapsedEventArgs elapsedEventArgs) {
            try {
                // Make sure we enter here only once
                lock (_lockStartAction) {
                    _actionStartTimer.Enabled = false;

                    List<ActionSchedulerAction> actionsToStart;
                    lock (_lockActions) {
                        actionsToStart = _actions.Where(a => a.StartAction < elapsedEventArgs.SignalTime && !a.IsRunning).ToList();
                    }

                    foreach (var action in actionsToStart) {
                        StartAction(action);
                    }
                }
            } finally {
                lock (_lockActions) {
                    if (_actions.Any()) {
                        _actionStartTimer.Enabled = true;
                    }
                }
            }
        }

        private void StartAction(ActionSchedulerAction action) {
            try {
                if (action.RunOnGuiThread) {
                    var dispatcher = System.Windows.Application.Current.Dispatcher;
                    var guiWorker = dispatcher.BeginInvoke(action.Action, DispatcherPriority.Background);
                    action.GuiWorker = guiWorker;
                    action.IsRunning = true;
                    guiWorker.Aborted += action.WasAborted;
                    guiWorker.Completed += action.WasCompleted;
                } else {
                    var worker = new Task(action.Action);
                    worker.GetAwaiter().OnCompleted(action.WasCompleted);
                    action.TaskWorker = worker;
                    action.IsRunning = true;
                    worker.Start();
                }
            } catch (Exception ex) {
                //_logger.LogError(MethodBase.GetCurrentMethod(), ex, () => "Action {0}.{1} could not be started!", action.Action.Method.DeclaringType != null ? action.Action.Method.DeclaringType.FullName : string.Empty, action.Action.Method.Name);
                RemoveAction(action);
            }
        }

        internal void RemoveAction(ActionSchedulerAction action) {
            lock (_lockActions) {
                if (_actions.Contains(action)) _actions.Remove(action);
            }
        }
    }
}
